# GAMEMAP.PY - The map and influence map classes
# HIT3046 AI for GAMES
# ------- Assignment 2
# Authors: Mike Blackney, 0906123
#          Alex Bruce, 5409438

import math

import pos
import astar
import utility

from defaults import *

class MAP (object):
    "This  singleton object contains information about a 2D grid of cells."

    class __impl: # Implementation using Singleton design pattern.
    
        def __init__( self ):
            # We have two lists:
            self.costs      = [] # The cost of each tile (-1 for blocked)
            self.lastpatrol = [] # The time at which each tile was last patrolled
            self.loslines   = [] # Traced line-of-sight debugging lines

            self.cost_blur = 0 # Time since last cost map blur
            self.time_blur = 0 # Time since last time map blur
            
            self.buildings  = [] # A list of all buildings in the map

            for index in range( MAP_SIZE[0] * MAP_SIZE[1] ):
                # Costs are initially neutral (no kills/no frags)
                self.costs.append(COST_NEUTRAL)
                # Time since patrol are all zero:
                self.lastpatrol.append(0)
                # LOS are all zero:
                self.loslines.append(0)

        def BlockingTile( self, position ):
            # Set this tile as blocking (-1)
            if position.x in range(MAP_SIZE[0]) and \
               position.y in range(MAP_SIZE[1]):
                self.costs[ position.x + position.y * MAP_SIZE[0] ] = -1

        def UnblockingTile( self, position ):
            # Set this tile as blocking (-1)
            if position.x in range(MAP_SIZE[0]) and \
               position.y in range(MAP_SIZE[1]):
                self.costs[ position.x + position.y * MAP_SIZE[0] ] = COST_NEUTRAL

        def isBlocked( self, position ):
            if not position.x in range(MAP_SIZE[0]) or \
               not position.y in range(MAP_SIZE[1]):
                return True
            return self.costs[ position.x + position.y * MAP_SIZE[0] ] == -1

        def SetLos( self, position ):
            # Set this tile as an LOS tile:
            if position.x in range(MAP_SIZE[0]) and \
               position.y in range(MAP_SIZE[1]):
                self.loslines[ position.x + position.y * MAP_SIZE[0] ] = -1
            else:
                # This is mostly for sanity check, but debugging could come handy:
                print "WEIRD LOS POSITION! = " + position
                raise SystemExit

        def ResetLos( self ):
            for index in range( MAP_SIZE[0] * MAP_SIZE[1] ):
                self.loslines[index] = 0

        def Reset( self ):
            for index in range( MAP_SIZE[0] * MAP_SIZE[1] ):
                self.costs[index]      = COST_NEUTRAL
                self.lastpatrol[index] = 0

        def CostsClone( self ):
            # Returns a list cloned from the current tile costs:
            ret_list = list( )
            for item in self.costs:
                ret_list.append(item)
            return ret_list
                
        def WeightedDest( self, start ):
            # Returns the coords of the oldest last-patrolled location
            oldest_val = None;
            equal_count = 0; # Total no. of tiles with same time value
            dest = pos.Pos( )
            for y in range( MAP_SIZE[1] ):
                for x in range( MAP_SIZE[0] ):
                    xypos = pos.Pos(x,y)
                    val = self.lastpatrol[x + y * MAP_SIZE[0]] \
                           * ( ( start - xypos ).length( ) )
                    # If it's an older location, it's not where we are now and
                    # it's not in the middle of a building, it's the current best:
                    if start != xypos \
                       and self.costs[x + y * MAP_SIZE[0]] != -1:
                        # If this value is older, use this tile:
                        if oldest_val is None \
                           or val <  oldest_val:
                            dest = xypos
                            oldest_val = val
                        # If the values are equal, randomly use this tile:
                        elif val == oldest_val:
                            equal_count += 1
                            if utility.RandomRange(0,equal_count) == 0:
                                dest = xypos
                                oldest_val = self.lastpatrol[x + y * MAP_SIZE[0]]
            if dest.x == 0 and dest.y == 0:
                print "0,0 with oldest: " + str(oldest_val)
            return dest

        def AddBuilding( self, building ):
            # Adds a building to the map:
            self.buildings.append( building )

        def BestHealStation( self, agent ):
            # Ask all the locations for their rewards and choose
            # one based on the one that produces the best result:
            best_score = None
            ret_building = None
            for building in self.buildings:
                # Check to see if the building would have any effect:
                if building.security_reward == 0 and \
                   building.heal_reward == 0:
                    # No effect, skip this building:
                    continue
                # Get the agent's desires:
                security = agent.d_security + building.security_reward
                strength = agent.d_strength + building.heal_reward
                discontent = math.hypot(security,strength)
                curr_score = security*security + strength*strength \
                             + discontent*discontent
                # Multiply the score by the building distance:
                curr_score *= ( building.location - agent.location ).length( )
                if best_score == None or \
                   best_score > curr_score:
                    best_score = curr_score
                    ret_building = building

            # DEBUG:
            #print "returning " + ret_building.name + " priority " + str(best_score)
            return ret_building

        def SetTime( self, position, time ):
            # Set the patrol time of a tile:
            if position.x in range(MAP_SIZE[0]) and \
               position.y in range(MAP_SIZE[1]):
                self.lastpatrol[position.x + position.y * MAP_SIZE[0]] = time
                
        def AddFrag( self, position, delta ):
            # Adds or subtracts from the cost of a tile:
            if position.x in range(MAP_SIZE[0]) and \
               position.y in range(MAP_SIZE[1]) and \
               self.costs[position.x+position.y * MAP_SIZE[0]] != -1:
                # Add the new cost:
                self.costs[position.x + position.y * MAP_SIZE[0]] += delta

        def Tick( self, currTime, deltaTime ):
            # Blurs the influence maps every while:
            self.cost_blur += deltaTime
            self.time_blur += deltaTime

            if self.cost_blur > COST_BLUR:
                self.cost_blur = 0
                self.Blur(self.costs)
            
            if self.time_blur > TIME_BLUR:
                self.time_blur = 0
                self.Blur(self.lastpatrol)

        def Blur( self, array ):
            # Blurs the array it's sent.
            # Make a list of old array vals:
            old_vals = []
            for item in array: old_vals.append(item)
            # Set the array to the blurred values:
            for y in range( MAP_SIZE[1] ):
                for x in range( MAP_SIZE[0] ):
                    ij_sum = 0
                    num = 0
                    # Don't blur blocked walls:
                    if array[x + y * MAP_SIZE[0]] == -1:
                        continue
                    for j in range( -1, 2 ):
                        for i in range( -1, 2 ):
                            point = pos.Pos( x+i, y+j )
                            if point.x < 0 or point.x >= MAP_SIZE[0] \
                               or point.y < 0 or point.y >= MAP_SIZE[1] \
                               or array[x+i + (y+j) * MAP_SIZE[0]] == -1:
                                continue
                            ij_sum += old_vals[point.x + point.y * MAP_SIZE[0]]
                            num += 1
                    array[x + y * MAP_SIZE[0]] = ij_sum / num

                
    # storage for the instance reference
    __instance = None

    def __init__(self): # Create singleton instance
        # Check whether we already have an instance
        if MAP.__instance is None:
            # Create and remember instance
            MAP.__instance = MAP.__impl( )

        # Store instance reference as the only member in the handle
        self.__dict__['_MAP__instance'] = MAP.__instance
        
    def __getattr__(self, attr): # Delegate access to implementation
        return getattr(self.__instance, attr)

    def __setattr__(self, attr, value): # Delegate access to implementation
        return setattr(self.__instance, attr, value)


